var model = sessionStorage.getItem('model');
var color = sessionStorage.getItem('color');

// 人数
var num = 0;
// 人称
var third = true;
// 是否开门
var door = false;
// 是否到终点
var end = false;
// 显示/关闭聊天
var show = false;
// 是否到达npc
var n = false;
var con = false;

var rob = false;
var rob_con = false;

// 计时器
var min = 0;
var sec = 0;
let timer = null;
var start = false;

function showChat() {
  if (show == false) {
    document.getElementById('talk_con_id').style.display = 'block';
    document.getElementById("bi-chat").className = "bi-x-circle";
    show = true;
  } else {
    document.getElementById('talk_con_id').style.display = 'none';
    document.getElementById("bi-chat").className = "bi-chat-dots";
    show = false;
  }
}

// 显示/关闭提示
var msg = false;
function showMsg() {
  if (msg == false) {
    document.getElementById('conten_id').style.display = 'block';
    document.getElementById("bi-info").className = "bi-x-circle";
    msg = true;
  } else {
    document.getElementById('conten_id').style.display = 'none';
    document.getElementById("bi-info").className = "bi-info-circle";
    msg = false;
  }
}


// 改变人称视角
function change() {
  if (third == true) {
    game.activeCamera = game.cameras.top;
    third = false;
  } else {
    game.activeCamera = game.cameras.third;
    third = true;
  }
}

// 确认收下地图
function shouxia() {
  n = false;
  con = true;
  document.getElementById('id01_n').style.display = 'none';
  document.getElementById('map').style.display = 'block';
  game.activeCamera = game.cameras.third;
}

// 监听键盘
window.addEventListener('message', (e) => {
  if (e.data == 83) {
    change();
  }
  if (e.data == 79) {
    // 开关门
    game.door.rotation.y = Math.PI / 2;
  }
}, false);

//对整个页面监听
document.onkeydown = function (e) {
  if (e.keyCode == 83) {
    change();
  }
  if (e.keyCode == 71) {
    game.activeCamera = game.cameras.god;
  }
}

// 随机创建迷宫的函数
function generateMaze(rows, cols) {
  let maze = new Array(rows)
  for (let r = 0; r < rows; r++) {
    maze[r] = new Array(cols).fill(1)
  }

  // 确定起点
  maze[0][1] = 0

  // 搜索和障碍相邻的所有坐标，并递归遍历
  function search(row, col) {
    let directions = [
      [0, -2],
      [0, 2],
      [-2, 0],
      [2, 0]
    ]

    directions.sort(() => Math.random() - 0.5) // 随机排序

    for (let d = 0; d < directions.length; d++) {
      let newRow = row + directions[d][0]
      let newCol = col + directions[d][1]

      if (newRow < 0 || newRow >= rows || newCol < 0 || newCol >= cols) {
        continue
      }

      if (maze[newRow][newCol] !== 0) {
        maze[newRow][newCol] = 0
        maze[row + directions[d][0] / 2][col + directions[d][1] / 2] = 0
        search(newRow, newCol)
      }
    }
  }

  search(0, 1)
  return maze
}

class Game {
  constructor() {
    if (!Detector.webgl) Detector.addGetWebGLMessage();

    this.modes = Object.freeze({
      NONE: Symbol("none"),
      PRELOAD: Symbol("preload"),
      INITIALISING: Symbol("initialising"),
      CREATING_LEVEL: Symbol("creating_level"),
      ACTIVE: Symbol("active"),
      GAMEOVER: Symbol("gameover")
    });
    this.mode = this.modes.NONE;

    this.container;
    this.player;
    this.npc;
    this.npc1;
    this.cameras;
    this.camera;
    this.scene;
    this.renderer;
    this.animations = {};
    this.assetsPath = 'assets/';

    this.remotePlayers = [];
    this.remoteColliders = [];
    this.initialisingPlayers = [];
    this.remoteData = [];

    this.messages = {
      text: [
        "Welcome to Blockland",
        "GOOD LUCK!"
      ],
      index: 0
    }

    this.container = document.createElement('div');
    this.container.style.height = '100%';
    document.body.appendChild(this.container);

    const sfxExt = SFX.supportsAudioType('mp3') ? 'mp3' : 'ogg';

    const game = this;
    this.anims = ['Walking', 'Walking Backwards', 'Turn', 'Running', 'Pointing', 'Talking', 'Pointing Gesture'];

    const options = {
      assets: [
        `${this.assetsPath}images/nx.jpg`,
        `${this.assetsPath}images/px.jpg`,
        `${this.assetsPath}images/ny.jpg`,
        `${this.assetsPath}images/py.jpg`,
        `${this.assetsPath}images/nz.jpg`,
        `${this.assetsPath}images/pz.jpg`
      ],
      oncomplete: function () {
        game.init();
      }
    }

    this.anims.forEach(function (anim) { options.assets.push(`${game.assetsPath}fbx/anims/${anim}.fbx`) });
    options.assets.push(`${game.assetsPath}fbx/town.fbx`);

    this.mode = this.modes.PRELOAD;

    this.clock = new THREE.Clock();

    const preloader = new Preloader(options);

    window.onError = function (error) {
      console.error(JSON.stringify(error));
    }
  }

  initSfx() {
    this.sfx = {};
    this.sfx.context = new (window.AudioContext || window.webkitAudioContext)();
    this.sfx.gliss = new SFX({
      context: this.sfx.context,
      src: { mp3: `${this.assetsPath}sfx/gliss.mp3`, ogg: `${this.assetsPath}sfx/gliss.ogg` },
      loop: false,
      volume: 0.3
    });
  }

  set activeCamera(object) {
    this.cameras.active = object;
  }

  init() {
    this.mode = this.modes.INITIALISING;

    this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 10, 200000);

    this.scene = new THREE.Scene();
    this.scene.background = new THREE.Color(0x00a0f0);

    const ambient = new THREE.AmbientLight(0xaaaaaa);
    this.scene.add(ambient);

    const light = new THREE.DirectionalLight(0xaaaaaa);
    light.position.set(30, 100, 40);
    light.target.position.set(0, 0, 0);

    light.castShadow = true;

    const lightSize = 500;
    light.shadow.camera.near = 1;
    light.shadow.camera.far = 500;
    light.shadow.camera.left = light.shadow.camera.bottom = -lightSize;
    light.shadow.camera.right = light.shadow.camera.top = lightSize;

    light.shadow.bias = 0.0039;
    light.shadow.mapSize.width = 1024;
    light.shadow.mapSize.height = 1024;

    this.sun = light;
    this.scene.add(light);

    // ground
    var mesh = new THREE.Mesh(new THREE.PlaneBufferGeometry(100000, 100000), new THREE.MeshPhongMaterial({ color: 0x999999, depthWrite: false }));
    mesh.rotation.x = - Math.PI / 2;
    mesh.receiveShadow = true;
    this.scene.add(mesh);

    var grid = new THREE.GridHelper(50000, 400, 0x000000, 0x000000);
    grid.material.opacity = 0.2;
    grid.material.transparent = true;
    this.scene.add(grid);

    // model
    const loader = new THREE.FBXLoader();
    const game = this;

    this.player = new PlayerLocal(this);
    // 创建npc
    this.npc = new NPC(this);
    this.npc1 = new NPC1(this);

    this.loadEnvironment(loader);
    this.createColliders();
    this.speechBubble = new SpeechBubble(this, "", 150);
    this.speechBubble.mesh.position.set(0, 50000000, -500);

    // 增加起点终点的提示框
    this.speechBubble1 = new message(this, "起点", 150);
    this.speechBubble1.mesh.position.set(490, 400, 320);

    this.speechBubble2 = new message(this, "终点", 150);
    this.speechBubble2.mesh.position.set(1000 + 245, 350, 500 * 20 - 500);

    this.joystick = new JoyStick({
      onMove: this.playerControl,
      game: this
    });

    this.renderer = new THREE.WebGLRenderer({ antialias: true });
    this.renderer.setPixelRatio(window.devicePixelRatio);
    this.renderer.setSize(window.innerWidth, window.innerHeight);
    this.renderer.shadowMap.enabled = true;
    this.container.appendChild(this.renderer.domElement);

    if ('ontouchstart' in window) {
      window.addEventListener('touchdown', (event) => game.onMouseDown(event), false);
    } else {
      window.addEventListener('mousedown', (event) => game.onMouseDown(event), false);
    }

    window.addEventListener('resize', () => game.onWindowResize(), false);
  }

  // 创建迷宫
  createColliders() {
    const game = this;
    const geometry = new THREE.BoxGeometry(500, 400, 500);
    const material = new THREE.MeshBasicMaterial({ map: new THREE.TextureLoader().load(`${this.assetsPath}images/wall1.jpg`) });
    const material_start = new THREE.MeshBasicMaterial({ map: new THREE.TextureLoader().load(`${this.assetsPath}images/wall.jpg`) });
    const material_door = new THREE.MeshBasicMaterial({ map: new THREE.TextureLoader().load(`${this.assetsPath}images/door.jpg`) });

    game.colliders = [];

    let maze = [
      // 迷宫地图
      //  ↓ 进的方向
      [1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
      [1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1],
      [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0],// 出口
      [1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1],
      [1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1],
      [1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1],
      [1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1],
      [1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1],
      [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1],
      [1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1],
      [1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1],
      [1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1],
      [1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1],
      [1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1],
      [1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1],
      [1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1],
      [1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1],
      [1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1],
      [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1],
      [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
    ];

    // 正确路径
    //  [1, +, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    // 	[1, +, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, +, +, +, 1, 1],
    // 	[1, +, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, +, 1, +, +, +],
    // 	[1, +, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, +, 1, 0, 1, 1],
    // 	[1, +, +, +, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, +, 1, 0, 1, 1],
    // 	[1, 1, 1, +, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, +, 1, 1, 1, 1],
    // 	[1, +, +, +, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, +, +, +, +, 1],
    // 	[1, +, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, +, 1],
    // 	[1, +, +, +, 1, +, +, +, 1, 0, 0, 0, 1, 0, 1, +, +, +, +, 1],
    // 	[1, 1, 1, +, 1, +, 1, +, 1, 1, 1, 1, 1, 0, 1, +, 1, 0, 1, 1],
    // 	[1, +, +, +, 1, +, 1, +, +, +, 1, +, +, +, +, +, 1, 0, 0, 1],
    // 	[1, +, 1, 1, 1, +, 1, 1, 1, +, 1, +, 1, 1, 1, 1, 1, 1, 1, 1],
    // 	[1, +, 1, +, +, +, 1, 0, 1, +, 1, +, 1, 0, 0, 0, 0, 0, 1, 1],
    // 	[1, +, 1, +, 1, 1, 1, 0, 1, +, 1, +, 1, 0, 1, 1, 1, 0, 1, 1],
    // 	[1, +, 1, +, 1, 0, 0, +, +, +, 1, +, +, +, +, +, 1, 0, 0, 1],
    // 	[1, +, 1, +, 1, 1, 1, +, 1, 1, 1, 1, 1, 1, 1, +, 1, 1, 1, 1],
    // 	[1, +, 1, +, +, +, 1, +, +, +, 1, +, +, +, 1, +, 0, 0, 1, 1],
    // 	[1, +, 1, 1, 1, +, 1, 1, 1, +, 1, +, 1, +, 1, +, 1, 1, 1, 1],
    // 	[1, +, +, +, +, +, 0, 0, 0, +, +, +, 1, +, +, +, 0, 0, 0, 1],
    // 	[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

    // 入口
    const box = new THREE.Mesh(geometry, material);
    box.position.set(-500, 250, 500);
    this.scene.add(box);
    game.colliders.push(box);

    // 出口
    const geometry_end = new THREE.BoxGeometry(10, 50, 500);
    const material_end = new THREE.MeshBasicMaterial({ color: 0xff0000 });
    const box1 = new THREE.Mesh(geometry_end, material_end);
    box1.position.set(1000, 200, 500 * 20 - 250);
    box1.rotation.y = Math.PI / 2
    this.scene.add(box1);

    // 迷宫
    for (let i = 0; i < 20; i++) {
      for (let j = 0; j < 20; j++) {
        const box = new THREE.Mesh(geometry, material);
        box.position.set(500 * i * maze[i][j], 250, 500 * j * maze[i][j]);
        this.scene.add(box);
        game.colliders.push(box);
      }

    }

    // 增加一个门
    const geometry3 = new THREE.BoxGeometry(10, 400, 250);
    const door = new THREE.Mesh(geometry3, material_door);
    game.door = door;
    door.position.set(500, 250, 500);
    game.colliders.push(door);
    this.scene.add(door);

    const geometry3_wall1 = new THREE.BoxGeometry(10, 400, 150);
    const wall1 = new THREE.Mesh(geometry3_wall1, material);
    wall1.position.set(500, 250, 300);
    game.colliders.push(wall1);
    this.scene.add(wall1);

    const wall2 = new THREE.Mesh(geometry3_wall1, material);
    wall2.position.set(500, 250, 700);
    game.colliders.push(wall2);
    this.scene.add(wall2);

  }

  loadEnvironment(loader) {
    const game = this;
    loader.load(`${this.assetsPath}fbx/town.fbx`, function (object) {
      game.environment = object;
      // game.colliders = [];
      // game.scene.add(object);
      object.traverse(function (child) {
        if (child.isMesh) {
          if (child.name.startsWith("proxy")) {
            game.colliders.push(child);
            child.material.visible = false;
          } else {
            child.castShadow = true;
            child.receiveShadow = true;
          }
        }
      });

      const tloader = new THREE.CubeTextureLoader();
      tloader.setPath(`${game.assetsPath}/images/`);

      var textureCube = tloader.load([
        'px.jpg', 'nx.jpg',
        'py.jpg', 'ny.jpg',
        'pz.jpg', 'nz.jpg'
      ]);

      game.scene.background = textureCube;

      game.loadNextAnim(loader);
    })
  }

  loadNextAnim(loader) {
    let anim = this.anims.pop();
    const game = this;
    loader.load(`${this.assetsPath}fbx/anims/${anim}.fbx`, function (object) {
      game.player.animations[anim] = object.animations[0];
      if (game.anims.length > 0) {
        game.loadNextAnim(loader);
      } else {
        delete game.anims;
        game.action = "Idle";
        game.mode = game.modes.ACTIVE;
        game.animate();
      }
    });
  }

  playerControl(forward, turn) {
    turn = -turn;
    if (forward > 0.3) {
      if (this.player.action != 'Walking' && this.player.action != 'Running') this.player.action = 'Walking';
    } else if (forward < -0.3) {
      if (this.player.action != 'Walking Backwards') this.player.action = 'Walking Backwards';
    } else {
      forward = 0;
      if (Math.abs(turn) > 0.1) {
        if (this.player.action != 'Turn') this.player.action = 'Turn';
      } else if (this.player.action != "Idle") {
        this.player.action = 'Idle';
      }
    }

    if (forward == 0 && turn == 0) {
      delete this.player.motion;
    } else {
      this.player.motion = { forward, turn };
    }

    this.player.updateSocket();
  }

  createCameras() {
    const offset = new THREE.Vector3(0, 80, 0);
    const front = new THREE.Object3D();
    front.position.set(112, 100, 400);
    front.parent = this.player.object;
    const top = new THREE.Object3D();
    top.position.set(0, 2500, -1050);
    top.parent = this.player.object;

    const god = new THREE.Object3D();
    god.position.set(0, 30000, -1050);
    god.parent = this.player.object;

    const first = new THREE.Object3D();
    first.position.set(0, 300, 0);
    first.parent = this.player.object;
    const back = new THREE.Object3D();
    back.position.set(0, 300, -1050);
    back.parent = this.player.object;
    const chat = new THREE.Object3D();
    chat.position.set(0, 200, -450);
    chat.parent = this.player.object;
    const wide = new THREE.Object3D();
    wide.position.set(178, 139, 1665);
    wide.parent = this.player.object;
    const third = new THREE.Object3D();
    third.position.set(0, 400, -300);
    third.parent = this.player.object;
    const collect = new THREE.Object3D();
    collect.position.set(40, 82, 94);
    collect.parent = this.player.object;
    this.cameras = { god, top, front, back, wide, third, collect, chat, first };
    this.activeCamera = this.cameras.third;
  }

  showMessage(msg, fontSize = 20, onOK = null) {
    const txt = document.getElementById('message_text');
    txt.innerHTML = msg;
    txt.style.fontSize = fontSize + 'px';
    const btn = document.getElementById('message_ok');
    const panel = document.getElementById('message');
    const game = this;
    if (onOK != null) {
      btn.onclick = function () {
        panel.style.display = 'none';
        onOK.call(game);
      }
    } else {
      btn.onclick = function () {
        panel.style.display = 'none';
      }
    }
    panel.style.display = 'flex';
  }

  onWindowResize() {
    this.camera.aspect = window.innerWidth / window.innerHeight;
    this.camera.updateProjectionMatrix();

    this.renderer.setSize(window.innerWidth, window.innerHeight);

  }

  updateRemotePlayers(dt) {
    if (this.remoteData === undefined || this.remoteData.length == 0 || this.player === undefined || this.player.id === undefined) return;

    const newPlayers = [];
    const game = this;
    //Get all remotePlayers from remoteData array
    const remotePlayers = [];
    const remoteColliders = [];

    this.remoteData.forEach(function (data) {
      // var labelDiv = document.createElement('div');
      // var label = new THREE.CSS2DObject(labelDiv);
      // label.position.set(data.x,data.y,data.z); // 设置标签位置
      // label.lookAt(label.position.clone().add(this.camera.position));
      // labelDiv.innerHTML = "player1"; // 设置标签文字
      // this.scene.add(label);

      
      if (game.player.id != data.id) {
        //Is this player being initialised?
        let iplayer;
        game.initialisingPlayers.forEach(function (player) {
          if (player.id == data.id) iplayer = player;
        });
        //If not being initialised check the remotePlayers array
        if (iplayer === undefined) {
          let rplayer;
          game.remotePlayers.forEach(function (player) {
            if (player.id == data.id) rplayer = player;
          });
          if (rplayer === undefined) {
            //Initialise player
            game.initialisingPlayers.push(new Player(game, data));
          } else {
            //Player exists
            remotePlayers.push(rplayer);
            remoteColliders.push(rplayer.collider);
          }
        }
      }
    });

    this.scene.children.forEach(function (object) {
      if (object.userData.remotePlayer && game.getRemotePlayerById(object.userData.id) == undefined) {
        game.scene.remove(object);
      }
    });

    this.remotePlayers = remotePlayers;
    this.remoteColliders = remoteColliders;
    this.remotePlayers.forEach(function (player) { player.update(dt); });
  }

  onMouseDown(event) {
    if (this.remoteColliders === undefined || this.remoteColliders.length == 0 || this.speechBubble === undefined || this.speechBubble.mesh === undefined) return;

    // calculate mouse position in normalized device coordinates
    // (-1 to +1) for both components
    const mouse = new THREE.Vector2();
    mouse.x = (event.clientX / this.renderer.domElement.clientWidth) * 2 - 1;
    mouse.y = - (event.clientY / this.renderer.domElement.clientHeight) * 2 + 1;

    const raycaster = new THREE.Raycaster();
    raycaster.setFromCamera(mouse, this.camera);

    const intersects = raycaster.intersectObjects(this.remoteColliders);
    const chat = document.getElementById('chat');

    if (intersects.length > 0) {
      const object = intersects[0].object;
      const players = this.remotePlayers.filter(function (player) {
        if (player.collider !== undefined && player.collider == object) {
          return true;
        }
      });
      if (players.length > 0) {
        const player = players[0];
        console.log(`onMouseDown: player ${player.id}`);
        this.speechBubble.player = player;
        this.speechBubble.update('');
        this.scene.add(this.speechBubble.mesh);
        this.chatSocketId = player.id;
        chat.style.bottom = '0px';
        this.activeCamera = this.cameras.chat;
        third = false;
      }
    } else {
      //Is the chat panel visible?
      if (chat.style.bottom == '0px' && (window.innerHeight - event.clientY) > 40) {
        console.log("onMouseDown: No player found");
        if (this.speechBubble.mesh.parent !== null) this.speechBubble.mesh.parent.remove(this.speechBubble.mesh);
        delete this.speechBubble.player;
        delete this.chatSocketId;
        chat.style.bottom = '-50px';
        this.activeCamera = this.cameras.third;
        third = true;
      } else {
        console.log("onMouseDown: typing");
      }
    }
  }

  getRemotePlayerById(id) {
    if (this.remotePlayers === undefined || this.remotePlayers.length == 0) return;

    const players = this.remotePlayers.filter(function (player) {
      if (player.id == id) return true;
    });

    if (players.length == 0) return;

    return players[0];
  }

  animate() {
    const game = this;
    const dt = this.clock.getDelta();

    requestAnimationFrame(function () { game.animate(); });

    this.updateRemotePlayers(dt);

    if (this.player.mixer != undefined && this.mode == this.modes.ACTIVE) this.player.mixer.update(dt);
    if (this.npc.mixer != undefined && this.mode == this.modes.ACTIVE) this.npc.mixer.update(dt);
    if (this.npc1.mixer != undefined && this.mode == this.modes.ACTIVE) this.npc1.mixer.update(dt);

    if (this.player.action == 'Walking') {
      const elapsedTime = Date.now() - this.player.actionTime;
      if (elapsedTime > 1000 && this.player.motion.forward > 0) {
        this.player.action = 'Running';
      }
    }

    if (end) {
      game.activeCamera = game.cameras.front;
      this.player.action = "Pointing Gesture";
      document.getElementById('id01').style.display = 'block';
    }

    if (n == true && con == false) {
      game.activeCamera = game.cameras.front;
      document.getElementById('id01_n').style.display = 'block';
    }

    if (rob == true) {
      this.npc1.action = "Pointing";
      game.activeCamera = game.cameras.front;
      document.getElementById('id01_r').style.display = 'block';
    }

    if (this.player.motion !== undefined) this.player.move(dt);

    if (this.cameras != undefined && this.cameras.active != undefined && this.player !== undefined && this.player.object !== undefined) {
      this.camera.position.lerp(this.cameras.active.getWorldPosition(new THREE.Vector3()), 0.05);
      const pos = this.player.object.position.clone();
      if (this.cameras.active == this.cameras.chat) {
        pos.y += 200;
      } else {
        pos.y += 300;
      }
      this.camera.lookAt(pos);
    }

    if (this.sun !== undefined) {
      this.sun.position.copy(this.camera.position);
      this.sun.position.y += 10;
    }

    if (this.speechBubble !== undefined) this.speechBubble.show(this.camera.position);

    this.renderer.render(this.scene, this.camera);
  }
}

class Player {
  constructor(game, options) {
    this.local = true;
    let model, colour, room;

    const colours = ['Black', 'Brown', 'White'];


    if (options === undefined) {
      const people = ['BeachBabe', 'BusinessMan', 'Doctor', 'FireFighter', 'Housewife', 'Policeman', 'Prostitute', 'Punk', 'RiotCop', 'Roadworker', 'Robber', 'Sheriff', 'Streetman', 'Waitress'];
      // model = people[m];
      model = sessionStorage.getItem("model");
      colour = sessionStorage.getItem("color");
      room = sessionStorage.getItem("room");
    } else if (typeof options == 'object') {
      this.local = false;
      this.options = options;
      this.id = options.id;
      model = options.model;
      colour = options.colour;
      room = options.room;
    } else {
      model = options;
    }
    this.model = model;
    this.colour = colour;
    this.room = room;
    this.game = game;
    this.animations = this.game.animations;

    const loader = new THREE.FBXLoader();
    const player = this;

    loader.load(`${game.assetsPath}fbx/people/${model}.fbx`, function (object) {

      object.mixer = new THREE.AnimationMixer(object);
      player.root = object;
      player.mixer = object.mixer;

      object.name = sessionStorage.getItem("username");

      object.traverse(function (child) {
        if (child.isMesh) {
          child.castShadow = true;
          child.receiveShadow = true;
        }
      });


      const textureLoader = new THREE.TextureLoader();

      textureLoader.load(`${game.assetsPath}images/SimplePeople_${model}_${colour}.png`, function (texture) {
        object.traverse(function (child) {
          if (child.isMesh) {
            child.material.map = texture;
          }
        });
      });

      // 修改人物初始位置
      player.object = new THREE.Object3D();
      player.object.position.set(0, 0, 500);
      // player.object.position.set(500 * 12 + 250, 0, 500 * 7);
      player.object.rotation.set(0, 89.52, 0);

      // 测试人物在终点
      // player.object.position.set(1000, 250, 500 * 19);

      player.object.add(object);
      if (player.deleted === undefined) game.scene.add(player.object);

      if (player.local) {
        game.createCameras();
        game.sun.target = game.player.object;
        game.animations.Idle = object.animations[0];
        if (player.initSocket !== undefined) player.initSocket();
      } else {
        const geometry = new THREE.BoxGeometry(100, 300, 100);
        const material = new THREE.MeshBasicMaterial({ visible: false });
        const box = new THREE.Mesh(geometry, material);
        box.name = "Collider";
        box.position.set(0, 150, 0);
        player.object.add(box);
        player.collider = box;
        player.object.userData.id = player.id;
        player.object.userData.remotePlayer = true;
        const players = game.initialisingPlayers.splice(game.initialisingPlayers.indexOf(this), 1);
        game.remotePlayers.push(players[0]);
      }

      if (game.animations.Idle !== undefined) player.action = "Idle";
    });
  }

  set action(name) {
    //Make a copy of the clip if this is a remote player
    if (this.actionName == name) return;
    const clip = (this.local) ? this.animations[name] : THREE.AnimationClip.parse(THREE.AnimationClip.toJSON(this.animations[name]));
    const action = this.mixer.clipAction(clip);
    action.time = 0;
    this.mixer.stopAllAction();
    this.actionName = name;
    this.actionTime = Date.now();

    action.fadeIn(0.5);
    action.play();
  }

  get action() {
    return this.actionName;
  }

  update(dt) {
    this.mixer.update(dt);

    if (this.game.remoteData.length > 0) {
      let found = false;
      for (let data of this.game.remoteData) {
        if (data.id != this.id) continue;
        //Found the player
        this.object.position.set(data.x, data.y, data.z);
        const euler = new THREE.Euler(data.pb, data.heading, data.pb);
        this.object.quaternion.setFromEuler(euler);
        this.action = data.action;
        found = true;
      }
      if (!found) this.game.removePlayer(this);
    }
  }
}

class NPC {
  constructor(game, options) {
    this.local = true;
    let model, colour, room;

    const colours = ['Black', 'Brown', 'White'];

    const people = ['BeachBabe', 'BusinessMan', 'Doctor', 'FireFighter', 'Housewife', 'Policeman', 'Prostitute', 'Punk', 'RiotCop', 'Roadworker', 'Robber', 'Sheriff', 'Streetman', 'Waitress'];
    // model = people[m];
    model = sessionStorage.getItem("model");
    colour = sessionStorage.getItem("color");
    room = sessionStorage.getItem("room");

    this.model = model;
    this.colour = colour;
    this.room = room;
    this.game = game;
    this.animations = this.game.animations;

    const loader = new THREE.FBXLoader();
    const player = this;

    loader.load(`${game.assetsPath}fbx/people/FireFighter.fbx`, function (object) {

      object.mixer = new THREE.AnimationMixer(object);
      player.root = object;
      player.mixer = object.mixer;

      object.name = sessionStorage.getItem("username");

      object.traverse(function (child) {
        if (child.isMesh) {
          child.castShadow = true;
          child.receiveShadow = true;
        }
      });


      const textureLoader = new THREE.TextureLoader();

      textureLoader.load(`${game.assetsPath}images/SimplePeople_FireFighter_White.png`, function (texture) {
        object.traverse(function (child) {
          if (child.isMesh) {
            child.material.map = texture;
          }
        });
      });

      // 修改人物初始位置
      player.object = new THREE.Object3D();
      player.object.position.set(500 * 12, 0, 500 * 7);
      player.object.rotation.set(0, 89.5, 0);

      player.object.add(object);
      if (player.deleted === undefined) game.scene.add(player.object);

      if (player.local) {
        game.createCameras();
        game.sun.target = game.player.object;
        game.animations.Idle = object.animations[0];
        if (player.initSocket !== undefined) player.initSocket();
      } else {
        const geometry = new THREE.BoxGeometry(100, 300, 100);
        const material = new THREE.MeshBasicMaterial({ visible: false });
        const box = new THREE.Mesh(geometry, material);
        box.name = "Collider";
        box.position.set(0, 150, 0);
        player.object.add(box);
        player.collider = box;
        player.object.userData.id = player.id;
        player.object.userData.remotePlayer = true;
        const players = game.initialisingPlayers.splice(game.initialisingPlayers.indexOf(this), 1);
        game.remotePlayers.push(players[0]);
      }

      if (game.animations.Idle !== undefined) player.action = "Idle";
    });
  }

  set action(name) {
    //Make a copy of the clip if this is a remote player
    if (this.actionName == name) return;
    const clip = (this.local) ? this.animations[name] : THREE.AnimationClip.parse(THREE.AnimationClip.toJSON(this.animations[name]));
    const action = this.mixer.clipAction(clip);
    action.time = 0;
    this.mixer.stopAllAction();
    this.actionName = name;
    this.actionTime = Date.now();

    action.fadeIn(0.5);
    action.play();
  }

  get action() {
    return this.actionName;
  }

  update(dt) {
    this.mixer.update(dt);

    if (this.game.remoteData.length > 0) {
      let found = false;
      for (let data of this.game.remoteData) {
        if (data.id != this.id) continue;
        //Found the player
        this.object.position.set(data.x, data.y, data.z);
        const euler = new THREE.Euler(data.pb, data.heading, data.pb);
        this.object.quaternion.setFromEuler(euler);
        this.action = data.action;
        found = true;
      }
      if (!found) this.game.removePlayer(this);
    }
  }
}

class NPC1 {
  constructor(game, options) {
    this.local = true;
    let model, colour, room;

    const colours = ['Black', 'Brown', 'White'];

    const people = ['BeachBabe', 'BusinessMan', 'Doctor', 'FireFighter', 'Housewife', 'Policeman', 'Prostitute', 'Punk', 'RiotCop', 'Roadworker', 'Robber', 'Sheriff', 'Streetman', 'Waitress'];
    // model = people[m];
    model = sessionStorage.getItem("model");
    colour = sessionStorage.getItem("color");
    room = sessionStorage.getItem("room");

    this.model = model;
    this.colour = colour;
    this.room = room;
    this.game = game;
    this.animations = this.game.animations;

    const loader = new THREE.FBXLoader();
    const player = this;

    loader.load(`${game.assetsPath}fbx/people/Robber.fbx`, function (object) {

      object.mixer = new THREE.AnimationMixer(object);
      player.root = object;
      player.mixer = object.mixer;

      object.name = sessionStorage.getItem("username");

      object.traverse(function (child) {
        if (child.isMesh) {
          child.castShadow = true;
          child.receiveShadow = true;
        }
      });


      const textureLoader = new THREE.TextureLoader();

      textureLoader.load(`${game.assetsPath}images/SimplePeople_Robber_White.png`, function (texture) {
        object.traverse(function (child) {
          if (child.isMesh) {
            child.material.map = texture;
          }
        });
      });

      // 修改人物初始位置
      player.object = new THREE.Object3D();
      player.object.position.set(500 * 10, 0, 500 * 18);
      player.object.rotation.set(0, 85, 0);

      player.object.add(object);
      if (player.deleted === undefined) game.scene.add(player.object);

      if (player.local) {
        game.createCameras();
        game.sun.target = game.player.object;
        game.animations.Idle = object.animations[0];
        if (player.initSocket !== undefined) player.initSocket();
      } else {
        const geometry = new THREE.BoxGeometry(100, 300, 100);
        const material = new THREE.MeshBasicMaterial({ visible: false });
        const box = new THREE.Mesh(geometry, material);
        box.name = "Collider";
        box.position.set(0, 150, 0);
        player.object.add(box);
        player.collider = box;
        player.object.userData.id = player.id;
        player.object.userData.remotePlayer = true;
        const players = game.initialisingPlayers.splice(game.initialisingPlayers.indexOf(this), 1);
        game.remotePlayers.push(players[0]);
      }

      if (game.animations.Idle !== undefined) player.action = "Idle";
    });
  }

  set action(name) {
    //Make a copy of the clip if this is a remote player
    if (this.actionName == name) return;
    const clip = (this.local) ? this.animations[name] : THREE.AnimationClip.parse(THREE.AnimationClip.toJSON(this.animations[name]));
    const action = this.mixer.clipAction(clip);
    action.time = 0;
    this.mixer.stopAllAction();
    this.actionName = name;
    this.actionTime = Date.now();

    action.fadeIn(0.5);
    action.play();
  }

  get action() {
    return this.actionName;
  }

  update(dt) {
    this.mixer.update(dt);

    if (this.game.remoteData.length > 0) {
      let found = false;
      for (let data of this.game.remoteData) {
        if (data.id != this.id) continue;
        //Found the player
        this.object.position.set(data.x, data.y, data.z);
        const euler = new THREE.Euler(data.pb, data.heading, data.pb);
        this.object.quaternion.setFromEuler(euler);
        this.action = data.action;
        found = true;
      }
      if (!found) this.game.removePlayer(this);
    }
  }
}

class PlayerLocal extends Player {
  constructor(game, model) {
    super(game, model);

    const player = this;
    const socket = io.connect();
    socket.on('setId', function (data) {
      player.id = data.id;
    });

    setInterval(function () {
      socket.emit('req_remoteData', { room: sessionStorage.getItem("room") });
    }, 40);

    function checkAdult(age) {
      return age.room == sessionStorage.getItem("room");
    }
    socket.on('remoteData', function (data) {
      game.remoteData = data;
      document.getElementById('num').innerHTML = '房间：' + sessionStorage.getItem("room") + '&emsp;&emsp;当前人数：' + data.length;
    });

    socket.on('deletePlayer', function (data) {
      if (data.room == sessionStorage.getItem("room")) {
        const players = game.remotePlayers.filter(function (player) {
          if (player.id == data.id) {
            return player;
          }
        });
        if (players.length > 0) {
          let index = game.remotePlayers.indexOf(players[0]);
          if (index != -1) {
            game.remotePlayers.splice(index, 1);
            game.scene.remove(players[0].object);
          }
        } else {
          index = game.initialisingPlayers.indexOf(data.id);
          if (index != -1) {
            const player = game.initialisingPlayers[index];
            player.deleted = true;
            game.initialisingPlayers.splice(index, 1);
          }
        }
      }
    });

    socket.on('chat message', function (data) {
      document.getElementById('chat').style.bottom = '0px';
      const player = game.getRemotePlayerById(data.id);
      game.speechBubble.player = player;
      game.chatSocketId = player.id;
      game.activeCamera = game.cameras.chat;
      game.speechBubble.update(data.message);
    });

    // 有人到达终点的播报
    var name = '';
    socket.on('end', function (data) {
      if (name == data.name) {

      } else {
        name = data.name;
        $('#end').append($('<h2>').text(data.name + "到达了终点，用时:" + data.time));
        var showContent = document.getElementById('end');
        showContent.scrollTop = showContent.scrollHeight;
      }
    });

    socket.on('door', function (data) {
      // 开门判定
      if (data == false) {
        if (door == true) {
          game.door.position.x = 500;
          game.door.position.z = 500;
          game.door.rotation.y = 0;
        }
        door = false;
      }

      // 关门判定
      if (data == true) {
        if (door == false) {
          game.door.translateX(125);
          game.door.translateZ(-125);
          game.door.rotation.y = Math.PI / 2;
        }
        door = true;
      }
    });

    $('#msg-form').submit(function (e) {
      socket.emit('chat message', { id: game.chatSocketId, message: $('#m').val() });
      $('#m').val('');
      return false;
    });

    this.socket = socket;
  }

  initSocket() {
    //console.log("PlayerLocal.initSocket");
    this.socket.emit('init', {
      model: this.model,
      colour: this.colour,
      room: sessionStorage.getItem("room"),
      x: this.object.position.x,
      y: this.object.position.y,
      z: this.object.position.z,
      h: this.object.rotation.y,
      pb: this.object.rotation.x
    });
  }

  updateSocket() {
    if (this.socket !== undefined) {
      //console.log(`PlayerLocal.updateSocket - rotation(${this.object.rotation.x.toFixed(1)},${this.object.rotation.y.toFixed(1)},${this.object.rotation.z.toFixed(1)})`);
      this.socket.emit('update', {
        x: this.object.position.x,
        y: this.object.position.y,
        z: this.object.position.z,
        h: this.object.rotation.y,
        pb: this.object.rotation.x,
        action: this.action,
        door: door
      })
    }
  }

  move(dt) {
    const pos = this.object.position.clone();
    pos.y += 60;
    let dir = new THREE.Vector3();
    this.object.getWorldDirection(dir);
    if (this.motion.forward < 0) dir.negate();
    let raycaster = new THREE.Raycaster(pos, dir);
    let blocked = false;
    const colliders = this.game.colliders;

    if (colliders !== undefined) {
      const intersect = raycaster.intersectObjects(colliders);
      if (intersect.length > 0) {
        if (intersect[0].distance < 50) blocked = true;
      }
    }

    if (!blocked) {
      if (this.motion.forward > 0) {
        const speed = (this.action == 'Running') ? 500 : 150;
        this.object.translateZ(dt * speed);
      } else {
        this.object.translateZ(-dt * 30);
      }
    }

    if (colliders !== undefined) {
      //cast left
      dir.set(-1, 0, 0);
      dir.applyMatrix4(this.object.matrix);
      dir.normalize();
      raycaster = new THREE.Raycaster(pos, dir);

      let intersect = raycaster.intersectObjects(colliders);
      if (intersect.length > 0) {
        if (intersect[0].distance < 50) this.object.translateX(100 - intersect[0].distance);
      }

      //cast right
      dir.set(1, 0, 0);
      dir.applyMatrix4(this.object.matrix);
      dir.normalize();
      raycaster = new THREE.Raycaster(pos, dir);

      intersect = raycaster.intersectObjects(colliders);
      if (intersect.length > 0) {
        if (intersect[0].distance < 50) this.object.translateX(intersect[0].distance - 100);
      }

      //cast down
      dir.set(0, -1, 0);
      pos.y += 200;
      raycaster = new THREE.Raycaster(pos, dir);
      const gravity = 30;

      intersect = raycaster.intersectObjects(colliders);
      if (intersect.length > 0) {
        const targetY = pos.y - intersect[0].distance;
        if (targetY > this.object.position.y) {
          //Going up
          this.object.position.y = 0.8 * this.object.position.y + 0.2 * targetY;
          this.velocityY = 0;
        } else if (targetY < this.object.position.y) {
          //Falling
          if (this.velocityY == undefined) this.velocityY = 0;
          this.velocityY += dt * gravity;
          this.object.position.y -= this.velocityY;
          if (this.object.position.y < targetY) {
            this.velocityY = 0;
            this.object.position.y = targetY;
          }
        }
      }
      else if (this.object.position.y > 0) {
        if (this.velocityY == undefined) this.velocityY = 0;
        this.velocityY += dt * gravity;
        this.object.position.y -= this.velocityY;
        if (this.object.position.y < 0) {
          this.velocityY = 0;
          this.object.position.y = 0;
        }
      }
    }

    this.object.rotateY(this.motion.turn * dt);

    this.updateSocket();

    // 绘制行走轨迹
    const geometry = new THREE.BoxGeometry(5, 4, 5);
    const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
    const box = new THREE.Mesh(geometry, material);
    box.position.set(this.object.position.x, 50, this.object.position.z);
    game.scene.add(box);

    // 开门判定
    if (this.object.position.x >= 400 && this.object.position.x < 500) {
      if (door == false) {
        game.door.translateX(125);
        game.door.translateZ(-125);
        game.door.rotation.y = Math.PI / 2;
        door = true;
      }
      this.socket.emit('door', {
        door: true
      });
    }

    // 关门判定
    if (this.object.position.x >= 700) {
      if (door == true) {
        game.door.position.x = 500;
        game.door.position.z = 500;
        game.door.rotation.y = 0;
        door = false;
      }
      this.socket.emit('door', {
        door: false
      });
    }

    // 开始计数器判定
    if (this.object.position.x > 500) {
      if (start == false) {
        start = true;
        var h1 = document.getElementById("dec");
        var c = document.getElementById("count");
        c.style.display = "block";
        h1.innerHTML = min + "分:" + sec + "秒"
        clearInterval(timer);
        timer = setInterval(() => {
          if (sec === 60) {
            ++min;
            sec = 0;
          }
          ++sec;
          h1.innerHTML = min + "分:" + sec + "秒"
        }, 1000);
      }
    }

    // 终点判定
    if (this.object.position.z > 500 * 20 - 250) {
      end = true;
      clearInterval(timer);
      this.socket.emit('end', {
        name: sessionStorage.getItem("username"),
        time: min + "分：" + sec + "秒"
      });
      // 关闭计时器
      var c = document.getElementById("count");
      c.style.display = "none";
      // 显示用时
      var t = document.getElementById("h3-time");
      t.innerHTML = "用时" + min + "分:" + sec + "秒";
    } else {
      end = false;
    }

    // 到达强盗判定
    if (this.object.position.z > 500 * 7 - 250 && this.object.position.z < 500 * 7 + 250 && this.object.position.x > 500 * 12 - 250 && this.object.position.x < 500 * 12 + 250) {
      n = true;
    } else {
      n = false;
    }

    // 到达npc判定
    if (this.object.position.z > 500 * 18 - 250 && this.object.position.x > 500 * 10 - 250 && this.object.position.x < 500 * 10 + 250) {
      rob = true;
      game.npc1.action = "Pointing";
    } else {
      rob = false;
    }
    // 	x: this.object.position.x,
    // 	y: this.object.position.y,
    // 	z: this.object.position.z,
  }
}

class SpeechBubble {
  constructor(game, msg, size = 1) {
    this.config = { font: 'Calibri', size: 24, padding: 10, colour: '#222', width: 256, height: 256 };

    const planeGeometry = new THREE.PlaneGeometry(size, size);
    const planeMaterial = new THREE.MeshBasicMaterial()
    this.mesh = new THREE.Mesh(planeGeometry, planeMaterial);
    game.scene.add(this.mesh);

    const self = this;
    const loader = new THREE.TextureLoader();
    loader.load(
      // resource URL
      `${game.assetsPath}images/speech.png`,

      // onLoad callback
      function (texture) {
        // in this example we create the material when the texture is loaded
        self.img = texture.image;
        self.mesh.material.map = texture;
        self.mesh.material.transparent = true;
        self.mesh.material.needsUpdate = true;
        if (msg !== undefined) self.update(msg);
      },

      // onProgress callback currently not supported
      undefined,

      // onError callback
      function (err) {
        console.error('An error happened.');
      }
    );
  }

  update(msg) {
    if (this.mesh === undefined) return;

    let context = this.context;

    if (this.mesh.userData.context === undefined) {
      const canvas = this.createOffscreenCanvas(this.config.width, this.config.height);
      this.context = canvas.getContext('2d');
      context = this.context;
      context.font = `${this.config.size}pt ${this.config.font}`;
      context.fillStyle = this.config.colour;
      context.textAlign = 'center';
      this.mesh.material.map = new THREE.CanvasTexture(canvas);
    }

    const bg = this.img;
    context.clearRect(0, 0, this.config.width, this.config.height);
    context.drawImage(bg, 0, 0, bg.width, bg.height, 0, 0, this.config.width, this.config.height);
    this.wrapText(msg, context);

    this.mesh.material.map.needsUpdate = true;
  }

  createOffscreenCanvas(w, h) {
    const canvas = document.createElement('canvas');
    canvas.width = w;
    canvas.height = h;
    return canvas;
  }

  wrapText(text, context) {
    const words = text.split(' ');
    let line = '';
    const lines = [];
    const maxWidth = this.config.width - 2 * this.config.padding;
    const lineHeight = this.config.size + 8;

    words.forEach(function (word) {
      const testLine = `${line}${word} `;
      const metrics = context.measureText(testLine);
      const testWidth = metrics.width;
      if (testWidth > maxWidth) {
        lines.push(line);
        line = `${word} `;
      } else {
        line = testLine;
      }
    });

    if (line != '') lines.push(line);

    let y = (this.config.height - lines.length * lineHeight) / 2;

    lines.forEach(function (line) {
      context.fillText(line, 128, y);
      y += lineHeight;
    });
  }

  show(pos) {
    if (this.mesh !== undefined && this.player !== undefined) {
      this.mesh.position.set(this.player.object.position.x, this.player.object.position.y + 380, this.player.object.position.z);
      this.mesh.lookAt(pos);
    }
  }
}

// 提示框类
class message {
  constructor(game, msg, size = 1) {
    this.config = { font: 'Calibri', size: 20, padding: 10, colour: '#222', width: 256, height: 256 };

    const planeGeometry = new THREE.PlaneGeometry(size, size);
    const planeMaterial = new THREE.MeshBasicMaterial()
    this.mesh = new THREE.Mesh(planeGeometry, planeMaterial);
    this.mesh.rotation.y = -Math.PI / 2;
    game.scene.add(this.mesh);

    const self = this;
    const loader = new THREE.TextureLoader();
    loader.load(
      // resource URL
      `${game.assetsPath}images/message.png`,

      // onLoad callback
      function (texture) {
        // in this example we create the material when the texture is loaded
        self.img = texture.image;
        self.mesh.material.map = texture;
        self.mesh.material.transparent = true;
        self.mesh.material.needsUpdate = true;
        if (msg !== undefined) self.update(msg);
      },

      // onProgress callback currently not supported
      undefined,

      // onError callback
      function (err) {
        console.error('An error happened.');
      }
    );
  }

  update(msg) {
    if (this.mesh === undefined) return;

    let context = this.context;

    if (this.mesh.userData.context === undefined) {
      const canvas = this.createOffscreenCanvas(this.config.width, this.config.height);
      this.context = canvas.getContext('2d');
      context = this.context;
      context.font = `${this.config.size}pt ${this.config.font}`;
      context.fillStyle = this.config.colour;
      context.textAlign = 'center';
      this.mesh.material.map = new THREE.CanvasTexture(canvas);
    }

    const bg = this.img;
    context.clearRect(0, 0, this.config.width, this.config.height);
    context.drawImage(bg, 0, 0, bg.width, bg.height, 0, 0, this.config.width, this.config.height);
    this.wrapText(msg, context);

    this.mesh.material.map.needsUpdate = true;
  }

  createOffscreenCanvas(w, h) {
    const canvas = document.createElement('canvas');
    canvas.width = w;
    canvas.height = h;
    return canvas;
  }

  wrapText(text, context) {
    const words = text.split('+');
    let line = '';
    const lines = [];
    const maxWidth = this.config.width - 2 * this.config.padding;
    const lineHeight = this.config.size + 8;

    words.forEach(function (word) {
      const testLine = `${line}${word} `;
      const metrics = context.measureText(testLine);
      const testWidth = metrics.width;
      if (testWidth > maxWidth) {
        lines.push(line);
        line = `${word} `;
      } else {
        line = testLine;
      }
    });

    if (line != '') lines.push(line);

    let y = (this.config.height - lines.length * lineHeight) / 2;

    lines.forEach(function (line) {
      context.fillText(line, 128, y);
      y += lineHeight;
    });
  }

  show(pos) {
    if (this.mesh !== undefined && this.player !== undefined) {
      this.mesh.position.set(this.player.object.position.x, this.player.object.position.y + 380, this.player.object.position.z);
      this.mesh.lookAt(pos);
    }
  }
}